#ifndef __tenacitas_log__logger__h__
#define __tenacitas_log__logger__h__


// =============================================================================
/** @file
    In this file is defined the @p tenacitas::log::logger class
*/

// =============================================================================
/** todo

 */


// =============================================================================
//                                 C++ Headers
#include <string>
#include <cstdint>
#include <mutex>
#include <map>
#include <utility>
#include <sstream>
#include <memory>
#include <functional>

// =============================================================================
//                                 3rds Headers
#include <boost/thread/thread.hpp>
#include <boost/shared_ptr.hpp>

// =============================================================================
//                                 Our Headers

/** In the <tt>level.h</tt> file we have the definition of the log levels  */
#include "level.h"

#include <tenacitas_exception/exception.h>

// =============================================================================
//                                 Namespaces
using namespace std;


// =============================================================================
//                                 Macro Contants
/** */


// =============================================================================
//                                 Macro Commands

/** This macro allows a message to be logged in @p tenacitas::log::level::trace level

    @param [in] logger is an instance of some specialization of @p tenacitas::log::logger class */
#define log_trace( logger ) logger.new_line( tenacitas::log::level::trace, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::debug level 

    @param [in] logger is an instance of some specialization of @p tenacitas::log::logger class */
#define log_debug( logger ) logger.new_line( tenacitas::log::level::debug, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::info level 

    @param [in] logger is an instance of some specialization of @p tenacitas::log::logger class */
#define log_info( logger ) logger.new_line( tenacitas::log::level::info, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::warn level 

    @param [in] logger is an instance of some specialization of @p tenacitas::log::logger class */
#define log_warn( logger ) logger.new_line( tenacitas::log::level::warn, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::error level 

    @param [in] logger is an instance of some specialization of @p tenacitas::log::logger class */
#define log_error( logger ) logger.new_line( tenacitas::log::level::error, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::critical level 

    @param [in] logger is an instance of some specialization of @p tenacitas::log::logger class */
#define log_critical( logger ) logger.new_line( tenacitas::log::level::critical, __FILE__, __func__, __LINE__ )


/** namespace of the organization */
namespace tenacitas {

    /** namespace of the project */
    namespace log {

        // =========================== Typedefs ===========================

        // ============================ Pre-Declarations ==================

        // ============================ Attributes ========================
        /** */

        // ============================ Functions =========================
        
        // ============================ Classes ===========================

        /** Base class that provides logging facilities

            @tparam t_media offers an abstraction about the media where the
            log messages will be printed, and it must implement:

            <tt>typedef ptr<tt>, which defines a pointer to
            a @p t_media object

            <tt>template <typename t_data> void write ( const t_data & )<tt>,
            which writes any type of data to the media

            <tt>void new_line( boost::thread_id p_thread_id,
                               level p_level,
                               const char * p_file,
                               const char * p_function,
                               int64_t p_line )</tt>, which starts a new log line
                               in the media 

            <tt>static t_media::ptr create( boost::thread::id )</tt>, which
            creates a pointer to a @p t_media object

            You can see an example of @p t_media implementation in
            tenacitas::log::file

            <b>A word to the programmer user</b>
            You, dear programmer, should avoid using the
            tenacitas::log::logger::new_line and <tt> operator<< <tt> members, and
            use the macros commands @p log_trace, @p log_debug, @p log_info,
            @p log_warn, @p log_error or @p log_critical instead.

            You should use the static method tenacitas::log::logger::set_level_all
            to define the tenacitas::log::level to be used by all the @p logger
            objects.

            You can change the tenacitas::log::level for all the @p logger objects
            to be created (not the ones already created), by calling
            tenacitas::log::logger::set_level_all again.

            When creating a @p logger, you can define a tenacitas::log::level to be
            used by only for it.

            You would better call tenacitas::log::logger::can_log before actualy
            logging a message to improve performance.

            @nosubgrouping
        */
        template <typename t_media>
        class logger {

        public:

            //    logger - Public Constructors

            /** Nothing to be done in the destructor */
            ~logger( ) { }

            /** Constructor

                @param [in] p_level is the tenacitas::log::level to be used
                by this @p logger object. Its default value is the log level
                defined for all the @p logger objects, defined using the
                @p set_level_all below, and retrieved via @p get_level_all

                @note Please, read the documentation for tenacitas::log::level
                to understand how log levels work in tenacitas::log

                @attention A @p logger object can not be passed to a function that
                will run in another thread. This is due to the concept that the
                tenacitas::log classes work to create log for a specific thread. So,
                if a @p logger object logs messages in a thread different from the
                one it was created, it will mix log messages of both threads.
            */
            logger( level p_level = logger::get_level_all( ) )
                : // saves the thread id for the current thread
                m_thread_id( boost::this_thread::get_id( ) ),
                // retrieves (or creates) a @p media object
                m_media( get_threads_loggers( ).get_media( this, m_thread_id ) ),
                // saves the log level defined
                m_level( p_level ) { }
            

            /** Copies are not-allowed */
            logger ( const logger & p_logger ) = delete;

            /** Moves are not-allowed */
            logger ( logger && p_logger ) = delete;

            //    logger - Public Accessors

            /** Sets log level for all the @p logger objects

                It only affects logger objects created after this method is called

                @param [in] p_level is the tenacitas::log::level to be used
                by all @p logger objects created after this method is called */
            static void set_level_all( const level & p_level_all ) {
                m_level_all = p_level_all;
            }

            /** @return the log level for all the @p logger objects */
            static level get_level_all( ) {
                return m_level_all;
            }
            

            /** Resets the log level defined to this @p logger object

                @param [in] p_level is the new tenacitas::log::level to be used
                by this @p logger object. */
            void set_level( const level & p_level ) {
                m_level = p_level;
            }

            /** @return the log level for this @p logger object */
            const level & get_level( ) const {
                return m_level;
            }

            /** Informs if a message log can be logged

                Before logging anything, one should, for performance sake, verify
                if the message will actualy be logged.
                <b>"Why?"</b>, you may ask
                Well, suppose a log such as:
                <tt> log_debug( logger ) << "very long string..."
                << a_double
                << "another long string";
                </tt>
                If the log level for the @p logger object is
                tenacitas::log::level::error, nothing will be logged. So, the work
                done to parse the parts of the log message will have been for
                nothing! Therefore, one should write:
                <tt>
                if ( logger.can_log(tenacitas::log::level::debug ) {
                log_debug( logger ) << "very long string..."
                << a_double
                << "another long string";
                }

                @param [in] p_level is the level one wants to check if a log
                will be logged

                @attention <b>tenacitas::log::level::error and
                tenacitas::log::level::critical will allways be logged, no matter
                the current log level. So, it is not necessary to check for the
                log level if one wants to use @p log_error or @p log_critical</b>
            */
            bool can_log( const level & p_level ) const {
                if ( ( p_level == level::error ) ||
                     ( p_level == level::critical ) ) {
                    return true;
                }
                return ( p_level >= m_level );
            }

            //    logger - Public Processors

            /** Starts a new log line

                @attention: One should not call  this method directly, but use the
                @p log_trace, @p log_debug, @p log_info, @p log_warn,
                @p logic_error or @p log_critical

                This methods inform the @p t_media class that a new line is
                starting. Depending on the concrete type of @p t_media, this can
                mean a new line in a file, a new record in a database table, or
                anything else.

                @param p_thread_id is the identifier of the thread that is starting
                a new log line
                @param [in] p_level is the tenacitas::log::level of the new line
                @param [in] p_file is the source file where the log is occurring
                @param [in] p_function is the function (method) that is log is
                occurring
                @param [in] p_line is the line it the @p p_file where the log is
                occurring

                @attention A @p logger object can not be passed to a function that
                will run in another thread. This is due to the concept that the
                tenacitas::log classes work to create log for a specific thread. So,
                if a @p logger object logs messages in a thread different from the
                one it was created, it will mix log messages for both threads.

                @throw std::string if this @p logger is being used in a
                different thread from where it was created
            */
            logger & new_line( level p_level,
                               const char * p_file,
                               const char * p_function,
                               int64_t p_line ) {

                // check if it is still in the same thread as it was created
                if( m_thread_id != boost::this_thread::get_id( ) ) {

                    // NO!

                    // to format the exception message
                    std::stringstream l_stream;

                    // the original thread
                    int64_t l_this_thread_id = th2int( m_thread_id );

                    // this 'other' thread
                    int64_t l_new_thread_id =
                        th2int( boost::this_thread::get_id( ) );
                    
                    l_stream << "This log is to be used with thread "
                             << std::dec << l_this_thread_id
                             << ", but its being used with thread "
                             << std::dec << l_new_thread_id ;
                    tenacitas::exception::exception
                        l_ex( make_tenacitas_exception( l_stream.str( ) ) );
                    std::cerr << l_ex.what( ) << std::endl;
                    throw ( l_ex );
                }
                
                // the current thread is the same as the one where this logger
                // object was created

                // but, the requested log level is allowed now?
                if ( can_log( p_level ) ) {
                    // ok! so we ask the @p t_media object to actually
                    // create a new log line
                    try {
                        m_media->new_line( m_thread_id, p_level, p_file,
                                           p_function, p_line ); 
                    }
                    catch ( std::exception & l_ex ) {
                        tenacitas::exception::exception
                            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

                        std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        
                        throw ( l_tenacitas_ex );
                    }
                }
                return *this;
            }

            /** Outputs any kind of data

                @tparam t_data is the type of the data to be logged

                @param p_data is the data to be logged
            */
            template <typename t_data>
            logger & operator << ( const t_data & p_data ) {
                try {
                    m_media->write( p_data );
                }
                catch( std::exception & l_ex ) {
                    tenacitas::exception::exception
                        l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

                    std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        
                    throw ( l_tenacitas_ex );
                }
                return *this;
            }

            /** Outputs any kind of data

                @tparam t_data is the type of the data to be logged

                @param p_data is the data to be logged
            */
            template <typename t_data>
            logger & operator << ( t_data && p_data ) {
                try {
                    m_media->write( p_data );
                }
                catch( std::exception & l_ex ) {
                    tenacitas::exception::exception
                        l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

                    std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        
                    throw ( l_tenacitas_ex );
                }
                return *this;
            }

        private:

            //    logger - Private Internal Types

            /** Function object to be called when a thread ends */
            struct at_thread_end {

                //    at_thread_end - Public Constructors

                /** Constructor
                 @param p_this is the @p logger class itself
                */
                at_thread_end( logger * p_this )
                : m_this ( p_this ){}
                

                //    at_thread_end - Public Operators

                /** Operator to be called when a thread ends */
                void operator( )( ) {
                    m_this->get_threads_loggers( ).
                        delete_logger( boost::this_thread::get_id( ) ); 
                }
            private:

                /** @p logger object that owns the medias associated to the
                    threads */
                logger * m_this;
            };

            /** Type for the @p t_media parameter */
            typedef t_media media;

            /** Type for the pointer to a @p t_media object */
            typedef typename media::ptr media_ptr;

            /** Type for a pointer to the logger class */
            typedef boost::shared_ptr<logger> ptr;

            /** This is collection of <tt>t_media::ptr </tt> objects and the
                thread to which they are associated */
            class threads_loggers {
            public:

                //    threads_loggers - Public Constructors

                /** Constructor */
                threads_loggers( )
                    : m_map( ),
                      m_mutex( ),
                      m_thread_group( ) {}

                /** Destructor  */
                ~threads_loggers( ) {
                    try {
                        {
                            // first we protect access to the map
                            std::lock_guard<std::mutex> lock( m_mutex );

                            // for every ( media, thread ) association...
                            iterator l_end = m_map.end( );
                            for ( iterator l_ite = m_map.begin( );
                                  l_ite != l_end;
                                  ++l_ite ) {

                                // save a reference to the media
                                media_ptr l_media = l_ite->second;

                                // erase the association from the map
                                m_map.erase( l_ite );

                                // start a new thread to handle a on_thread_end event 
                                boost::thread * l_thread =
                                    new boost::thread( &media::on_thread_end, l_media ); 

                                // add this thread to a thread group
                                m_thread_group.add_thread( l_thread );

                            }

                            // here we can release the mutex
                        }
                    
                        // sit back, relax, and wait for all the threads to end
                        m_thread_group.join_all( );
                    }
                    catch ( tenacitas::exception::exception & l_ex ) {
                        std::cerr << l_ex.what( ) << std::endl;
                    }
                    catch ( std::exception & l_ex ) {
                        tenacitas::exception::exception
                            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

                        std::cerr << l_tenacitas_ex.what( ) << std::endl;
                    }
                }

                /** Retrieves a @p media_ptr associated to a thread

                    This method will search for a @p media_ptr in the collection of
                    @p media_ptr, and it will return it; or it will create a
                    @p media_ptr, associate it to the thread, and insert it into
                    the collection

                    @param [in] p_thread_id is the thread one wants to retrieve the
                    @p media_ptr associated

                */
                media_ptr get_media( logger * p_this,
                                     boost::thread::id p_thread_id ) {

                    try {

                        // already created?
                        iterator l_ite = m_map.find( p_thread_id );

                        if ( l_ite != m_map.end( ) ) {
                            // yes!
                            return ( l_ite->second );
                        }

                        // inserting the logger into the collection of loggers,
                        // associating to theirs threads

                        media_ptr l_media = media::create( p_thread_id );

                        std::pair<boost::thread::id, media_ptr> l_pair( p_thread_id,
                                                                        l_media );

                        std::lock_guard<std::mutex> lock(m_mutex);

                        // I can lock only here, because the thread l_th_id can not be
                        // modified in another thread
                        boost::this_thread::at_thread_exit( at_thread_end( p_this ) );

                        // returning a reference to the just inserted logger
                        return (  m_map.insert( l_pair ).first->second );
                    }
                    catch ( tenacitas::exception::exception & l_ex ) {
                        std::cerr << l_ex.what( ) << std::endl;
                        throw l_ex;
                    }
                    catch ( std::exception & l_ex ) {
                        tenacitas::exception::exception
                            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

                        std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        throw l_tenacitas_ex;
                    }
                }

                /** Deletes a <tt>(media_ptr, boost::thread::id) from the collection

                    @param [in] p_thread_id is the thread to be removed, together to
                    its associated @p media_ptr from the collection
                */
                void delete_logger( boost::thread::id p_thread_id ) {

                    try {
                        iterator l_ite = m_map.find( p_thread_id );

                        if ( l_ite != m_map.end( ) ) {

                            media_ptr l_media = l_ite->second;

                            {
                                // I can lock only here, because exclusion of p_thread_id can
                                // not happen in another thread
                                std::lock_guard<std::mutex> lock( m_mutex );

                                m_map.erase( l_ite );
                            }

                            boost::thread * l_thread =
                                new boost::thread( &media::on_thread_end, l_media ); 
                    
                            m_thread_group.add_thread( l_thread );

                        }
                    }
                    catch ( tenacitas::exception::exception & l_ex ) {
                        std::cerr << l_ex.what( ) << std::endl;
                        throw l_ex;
                    }
                    catch ( std::exception & l_ex ) {
                        tenacitas::exception::exception
                            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

                        std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        throw l_tenacitas_ex;
                    }

                }

            private:

                /** Type of the collection of the thread and the media where log
                    messages will be recorded */
                typedef std::map<boost::thread::id, media_ptr> map;

                /** The type of iterator of the collection above */
                typedef typename map::iterator iterator;

                /** collection of the thread and the media where log messages
                    will be recorded */
                map m_map;

                /** To protect access to @p m_threads_loggers */
                std::mutex m_mutex;

                /** */
                boost::thread_group m_thread_group;
            };


            //    logger - Private Accessors

            /** The collection of boost::thread::id and the @p media_ptr
                associated to it */
            threads_loggers & get_threads_loggers( ) {
                static threads_loggers l_threads_loggers;
                return l_threads_loggers;
            }

            //    logger - Private Attributes            

            /** Thread where this @p logger was created */
            boost::thread::id m_thread_id;

            /** Class that actually logs the message */
            media_ptr m_media;

            /** Log level of this @p logger object */
            level m_level;


            /** Log level commom to all the @p logger objects */
            static level m_level_all;

        };
    }
}

using namespace tenacitas::log;


/** Default log level for all the @p logger objects is
    tenacitas::log::level::info */
template <typename t_media>
level
logger<t_media>::
m_level_all( level::info );


#endif
